﻿namespace MapleRock.P2PEmulator.Core
{
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Linq;
  using System.Net;
  using System.Text;

  public sealed class Network
  {
    public const int RunDurationMilliseconds = 15;

    private XmlReport report = null;

    private DateTime beginTimestamp = default(DateTime);

    private int runIndex = int.MinValue;

    public delegate void CompletionStageDelegate(Network network, int runsTotal, int runsCompleted);

    public event CompletionStageDelegate OnCompletionStage;

    public PeerBehaviour PeersBehaviour { get; set; }

    public XmlReport Report
    {
      get { return this.report; }
    }

    public NetworkState State { get; set; }

    public void Go(int runs)
    {
      Debug.Assert(runs > 0, "runs > 0");
      Debug.Assert(runs <= 100000, "runs <= 100000");
      Debug.Assert(this.runIndex >= 0, "this.runIndex >= 0");

      for (var index = 0; index < runs; ++index)
      {
        this.Run(this.runIndex + index);

        this.State.IncrementTimer(Network.RunDurationMilliseconds);

        if (null != this.OnCompletionStage)
        {
          this.OnCompletionStage.Invoke(this, runs, this.runIndex + index);
        }
      }

      this.runIndex += runs;
    }

    public void Initialize(XmlInputData inputData)
    {
      Debug.Assert(null != inputData, "null != inputData");

      this.report = new XmlReport();
      this.report.InputData = inputData;

      Dictionary<uint, City> cities = null;
      AdjacencyTable citiesAdjacency = null;

      this.GenerateCities(inputData, out cities, out citiesAdjacency);

      Debug.Assert(null != cities, "null != cities");
      Debug.Assert(null != citiesAdjacency, "null != citiesAdjacency");

      Dictionary<uint, Peer> peers = null;

      this.GeneratePeers(inputData, cities, out peers, this.PeersBehaviour);

      Debug.Assert(null != peers, "null != peers");

      this.State = new NetworkState(
        cities, 
        citiesAdjacency, 
        inputData.Dimensions, 
        peers, 
        inputData.UpdDeliveryFailureProbability);
    }

    public void PrepareRun()
    {
      this.State.ResetTimer();

      this.runIndex = 0;

      this.beginTimestamp = DateTime.Now;
    }

    public void ResetState()
    {
      this.State.Transport.ResetState();

      foreach (var peer in this.State.Peers.Values)
      {
        peer.ResetState();
      }

      this.State.ActivePeers = new HashSet<Peer>(this.State.Peers.Values);
    }

    public void Run(int index)
    {
      Debug.Assert(index >= 0, "index >= 0");

      var inactivationList = new HashSet<Peer>();

      foreach (var peer in this.State.ActivePeers)
      {
        var isInactive =
          ((peer.Incoming.Count == 0) &&
           (peer.State == PeerState.MainLoop));
        if (isInactive)
        {
          inactivationList.Add(peer);
        }
        else
        {
          peer.OnIdle(this.State);
        }

        peer.ProcessOutgoing(this.State, Network.RunDurationMilliseconds);
      }

      foreach (var inactivePeer in inactivationList)
      {
        this.State.ActivePeers.Remove(inactivePeer);
      }

      this.State.Transport.EmulateDataTransfer(Network.RunDurationMilliseconds, this.State);
    }

    public void SaveRunResults()
    {
      var endTimestamp = DateTime.Now;

      this.Report.RuntimeSeconds = (endTimestamp - this.beginTimestamp).Seconds; 
    }

    private static double[] GeneratePeerPosition(City city, Random random)
    {
      var angle = (Math.PI * 2 * random.NextDouble());
      var distance = (city.Radius * random.NextDouble());
      var x = distance * Math.Cos(angle);
      var y = distance * Math.Sin(angle);

      return new double[] { x + city.Latitude, y + city.Longitude };
    }

    private static IPAddress GeneratePeerIp(uint id)
    {
      var ipAddress = new uint[4] { 10, 10, 10, 10 };
      ipAddress[3] += id;

      if (ipAddress[3] >= 255)
      {
        var temp = ipAddress[3];
        ipAddress[3] = (temp % 255) + 1;
        ipAddress[2] += temp / 255;
      }

      if (ipAddress[2] >= 255)
      {
        var temp = ipAddress[2];
        ipAddress[2] = (temp % 255) + 1;
        ipAddress[1] += temp / 255;
      }

      if (ipAddress[1] >= 255)
      {
        var temp = ipAddress[1];
        ipAddress[1] = (temp % 255) + 1;
        ipAddress[0] += temp / 255;
      }

      Debug.Assert(ipAddress[0] < 255, "ipAddress[0] < 255");

      return new IPAddress(new byte[4] 
      { 
        Convert.ToByte(ipAddress[0]), 
        Convert.ToByte(ipAddress[1]), 
        Convert.ToByte(ipAddress[2]), 
        Convert.ToByte(ipAddress[3]) 
      });
    }

    private void GatherIPAddresses(
      List<KeyValuePair<IPAddress, uint>> addresses, 
      List<uint> visitedCities, 
      uint nextCityId,
      uint hops)
    {
      Debug.Assert(null != addresses, "null != addresses");
      Debug.Assert(null != visitedCities, "null != visitedCities");
      Debug.Assert(nextCityId > 0, "nextCityId > 0");

      Debug.Assert(!visitedCities.Contains(nextCityId), "!visitedCities.Contains(nextCityId)");
      visitedCities.Add(nextCityId);

      var peersAddresses =
        this.State.Peers.Values.Where(peer => (peer.CityId == nextCityId)).Select(peer => peer.Address);
      foreach (var a in peersAddresses)
      {
        addresses.Add(new KeyValuePair<IPAddress, uint>(a, hops));
      }

      foreach (var city in this.State.Cities.Values)
      {
        if (nextCityId == city.Id)
        {
          continue;
        }

        var citiesConnected = this.State.CitiesAdjacency.IsAdjacent(nextCityId, city.Id);
        if (!citiesConnected)
        {
          continue;
        }

        if (visitedCities.Contains(city.Id))
        {
          continue;
        }

        this.GatherIPAddresses(addresses, visitedCities, city.Id, hops + 1);
      }
    }

    private void GenerateCities(
      XmlInputData inputData, 
      out Dictionary<uint, City> cities,
      out AdjacencyTable citiesAdjacency)
    {
      cities = new Dictionary<uint, City>();
      CitiesGenerator.Generate(inputData, ref cities);

      foreach (var city in cities.Values)
      {
        var xmlCity = new XmlCity();
        xmlCity.Id = city.Id;
        xmlCity.Latitude = city.Latitude;
        xmlCity.Longitude = city.Longitude;
        xmlCity.Radius = city.Radius;
        xmlCity.Size = city.Size;
        this.report.Cities.Add(xmlCity);
      }

      citiesAdjacency = this.GenerateCitiesAdjacencyTable(cities);
    }

    private AdjacencyTable GenerateCitiesAdjacencyTable(Dictionary<uint, City> cities)
    {
      // we're need place for the cities identifiers plus zero identifier
      var citiesAdjacency = new AdjacencyTable(cities.Count + 1);

      // if there is just one city
      if (cities.Count == 1)
      {
        // cities adjacency table will be empty

        this.report.CitiesAdjacency = citiesAdjacency.ToString();

        return citiesAdjacency;
      }

      foreach (var city1 in cities.Values)
      {
        var minDistance = double.MaxValue;
        var adjacentCity = null as City;

        foreach (var city2 in cities.Values)
        {
          if (city1 == city2)
          {
            continue;
          }

          if (citiesAdjacency.IsAdjacent(city1.Id, city2.Id))
          {
            break;
          }

          var distance = city1.GetDistance(city2);
          if (distance < minDistance)
          {
            minDistance = distance;
            adjacentCity = city2;
          }
        }

        // if that city was already properly connected
        if (null == adjacentCity)
        { 
          // satisfy that it's true
          Debug.Assert(
            citiesAdjacency.GetAdjacentEntriesCount(city1.Id) > 0,
            "citiesAdjacency.GetAdjacentEntriesCount(city1.Id) > 0");

          // and follow to the next city
          continue;
        }

        citiesAdjacency.Connect(city1.Id, adjacentCity.Id);
      }

      this.report.CitiesAdjacency = citiesAdjacency.ToString();

      return citiesAdjacency;
    }

    private void GeneratePeers(
      XmlInputData inputData, 
      Dictionary<uint, City> cities,
      out Dictionary<uint, Peer> peers,
      PeerBehaviour behaviour)
    {
      Debug.Assert(null != cities, "null != cities");
      Debug.Assert(0 != cities.Count, "0 != cities.Count");

      var infoSpaceBounds = new KeyValuePair<double, double>[inputData.Dimensions];
      var infoSpaceBoundsParts = inputData.InfoSpaceBounds.Split(' ');
      Debug.Assert(
        infoSpaceBoundsParts.Length == (inputData.Dimensions * 2),
        "infoSpaceBoundsParts.Length == (inputData.Dimensions * 2)");
      for (var i = 0; i < inputData.Dimensions; ++i)
      {
        var low = double.Parse(infoSpaceBoundsParts[(i * 2) + 0]);
        var high = double.Parse(infoSpaceBoundsParts[(i * 2) + 1]);

        infoSpaceBounds[i] = new KeyValuePair<double, double>(low, high);
      }

      peers = new Dictionary<uint, Peer>();

      var random = new Random();
      var peerIndex = 1u;
      foreach (var city in cities.Values)
      {
        for (var index = 0; index < city.Size; ++index)
        {
          var peer = new Peer(
            peerIndex++, 
            city.Id, 
            Network.GeneratePeerIp(peerIndex),
            new double[inputData.Dimensions],
            Network.GeneratePeerPosition(city, random),
            1,
            inputData.MaxPeerLinksAllowed,
            behaviour);

          for (var dimIndex = 0; dimIndex < inputData.Dimensions; ++dimIndex)
          {
            var diff = (infoSpaceBounds[dimIndex].Value - infoSpaceBounds[dimIndex].Key);

            var dimValue =
              infoSpaceBounds[dimIndex].Key + (random.NextDouble() * diff);
            peer.AbsolutePosition[dimIndex] = dimValue;
          }

          peers.Add(peer.Id, peer);

          var xmlPeer = new XmlPeer();
          xmlPeer.Id = peer.Id;
          xmlPeer.CityId = city.Id;
          xmlPeer.IpAddress = peer.Address.ToString();

          xmlPeer.Position = string.Empty;
          foreach (var c in peer.AbsolutePosition)
          {
            xmlPeer.Position += (c + " ");
          }

          xmlPeer.Position = xmlPeer.Position.TrimEnd();

          this.report.InitialState.Add(xmlPeer);
        }
      }
    }
  }
}
